// compiler的核心代码
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');
const path = require("path");

const fs = require("fs");
const { SyncHook } = require("tapable"); // 可以简单将SyncHook()方法理解称为一个Emitter Event类。
const { toUnixPath, getSourceCode } = require('../utils');
const { tryExtensions } = require('../utils');

class Compiler {
  constructor(options) {
    this.options = options;
    /** 相对路径跟路径 Context参数 */
    this.rootPath = this.options.context || toUnixPath(process.cwd());
    /** 创建 plugin hooks */
    this.hooks = {
      // 开始编译时的钩子
      run: new SyncHook(),
      // 输出 asset 到 output 目录之前执行 (写入文件之前)
      emit: new SyncHook(),
      // 在 compilation 完成时执行 全部完成编译执行
      done: new SyncHook(),
    };
    /** 保存所有入口模块对象 */
    this.entries = new Set();
    /** 保存所有依赖模块对象 */
    this.modules = new Set();
    /** 所有的代码块对象 */
    this.chunks = new Set();
    /** 存放本次产出的文件对象 */
    this.assets = new Map();
    /** 存放本次编译所有产出的文件名 */
    this.files = new Set();
    /** 当前模块代码（可能已经被加工过） */
    this.moduleCode;
  }

  // run方法启动编译
  // 同时run方法接受外部传递的callback
  run(callback) {
    // 当调用run方式时，触发开始编译的plugin；告诉订阅者，发布开始执行的订阅
    // 通过this.hooks.run.call()执行关于run的所有tap监听方法，从而触发对应的plugin逻辑
    this.hooks.run.call();

    // 获取入口配置对象
    const entry = this.getEntry();
    // 编译入口文件
    this.buildEntryModule(entry);
    // 导出列表，之后将每个chunk转化为单独的文件加入到输出列表assets中
    this.exportFile(callback)
  }

  /** 获取入口文件路径 */
  getEntry() {
    let entry = Object.create(null);
    const { entry: optionsEntry } = this.options;
    if (typeof optionsEntry === "string") {
      entry['main'] = optionsEntry;
    } else {
      entry = optionsEntry;
    }
    // 将entry变成绝对路径
    Object.keys(entry).forEach(key => {
      const value = entry[key];
      if (!path.isAbsolute(value)) {
        // 转换为绝对路径的同时统一路径分隔符为 / 
        entry[key] = toUnixPath(path.join(this.rootPath, value));
      }
    })
    return entry;
  }

  /** 编译入口文件 */
  buildEntryModule(entry) {
    Object.keys(entry).forEach(entryName => {
      const entryPath = entry[entryName];
      const entryObj = this.buildModule(entryName, entryPath);
      this.entries.add(entryObj);

      // 根据当前入口文件和模块的相互依赖关系，组装成为一个个包含当前入口所有依赖模块的chunk
      this.buildUpChunk(entryName, entryObj);
    })
    // console.info("entries", this.entries)
    // console.info("modules", this.modules)
    // console.info("chunks", this.chunks)
  }

  /** 模块编译方法 */
  buildModule(moduleName, modulePath) {
    // 1.读取文件原始代码
    const originSourceCode = this.originSourceCode = fs.readFileSync(modulePath, "utf-8");
    // moduleCode为修改后的代码
    this.moduleCode = originSourceCode;
    // 2.调用Loader进行处理
    this.handleLoader(modulePath);
    // 3.调用webpack 进行模块编译 获得最终的module对象
    const module = this.handleWebpackCompiler(moduleName, modulePath)
    // 4.返回对应的module
    return module
  }

  /** 匹配Loader处理 */
  handleLoader(modulePath) {
    const matchLoaders = [];
    // 1.获取所有传入的Loader规则
    const rules = this.options.module.rules;
    rules.forEach(loader => {
      const testRule = loader.test;
      if (testRule.test(modulePath)) {
        // 仅考虑loader { test:/\.js$/g, use:['babel-loader'] }, { test:/\.js$/, loader:'babel-loader' }
        if (loader.loader) {
          matchLoaders.push(loader.loader)
        } else {
          matchLoaders.push(...loader.use)
        }
      }
    });
    // 2. 倒序执行loader传入源代码
    for (let i = matchLoaders.length - 1; i >=0; i--) {
      // 目前我们外部仅支持传入绝对路径的loader模式
      // require引入对应loader
      const loaderFn = require(matchLoaders[i]);
      // 通过loader同步处理我的每一次编译的moduleCode
      this.moduleCode = loaderFn(this.moduleCode);
    }
  }
  /** 模块编译 */
  handleWebpackCompiler(moduleName, modulePath) {
    // 将当前模块相对于项目启动根目录计算出相对路径 作为模块ID
    const moduleId = './' + path.relative(this.rootPath, modulePath);
    // 创建模块对象
    const module = {
      id: moduleId, // 当前模块相对于根路径的模块路径
      dependencies: new Set(), // 该模块所依赖模块绝对路径地址
      name: [moduleName], // 该模块所属的入口文件
      _source: "", // 代码
    }

    // 调用babel分析我们的代码
    const ast = parser.parse(this.moduleCode, {
      sourceType: "module",
    });
    // 深度优化，遍历语法Tree
    traverse(ast, {
      // 调用表达式。判断require这个调用表达式
      CallExpression: (nodePath) => {
        const node = nodePath.node;
        if (node.callee.name === "require") {
          // 获取源代码中引入模块相对路径
          const moduleName = node.arguments[0].value;
          // 寻找模块绝对路径 相对模块路径 + require()对应相对路径
          const moduleDirName = path.dirname(modulePath);
          const absolutePath = tryExtensions(
            path.join(moduleDirName, moduleName),
            this.options.resolve.extensions,
            moduleName,
            moduleDirName
          );
          // 生成moduleId - 针对于跟路径的模块ID 添加进入新的依赖模块路径。作为key
          const moduleId = './' + path.relative(this.rootPath, absolutePath);
          // 通过babel修改源代码中的require变成__webpack_require__语句
          node.callee = t.identifier('__webpack_require__');
          // 修改源代码中require语句引入的模块 全部修改变为相对于跟路径来处理
          node.arguments = [t.stringLiteral(moduleId)];

          const alreadyModules = Array.from(this.modules).map(item => item.id);
          if (!alreadyModules.includes(moduleId)) {
            // 为当前模块添加require语句造成的依赖(内容为相对于根路径的模块ID)
            module.dependencies.add(moduleId);
          } else {
            // 已经存在的，虽然不进行添加到模块编译，但是需要更新这个模块依赖的入口
            this.modules.forEach(value => {
              if (value.id === moduleId) {
                value.name.push(moduleName);
              }
            })
          }
        }
      }
    });

    // 生成修改后的代码
    const { code } = generator(ast);
    // 为当前模块挂载新的生成的代码
    module._source = code;
    // 递归依赖深度遍历 存在依赖模块则加入
    module.dependencies.forEach(dependencyPath => {
      const depModule = this.buildModule(moduleName, dependencyPath);
      // 将编译后的任何依赖模块对象加入到modules对象中
      this.modules.add(depModule);
    });
    return module;
  }
  /** 根据入口文件和依赖模块组装chunks
   * @description
   * @author luyaxiang
   * @date 2021/12/16
   * @param {*} entryName 入口路径名称;如：main
   * @param {*} entryObj buildModule方法处理后的对象
   * @memberof Compiler
   */
  buildUpChunk(entryName, entryObj) {
    const chunk = {
      name: entryName,
      entryModule: entryObj,
      modules: Array.from(this.modules).filter(i => i.name.includes(entryName)), // 寻找与当前entry有关的所有module
    };
    this.chunks.add(chunk);
  }

  /** 将chunk加入输出列表中 */
  exportFile(callback) {
    const output = this.options.output;
    // 根据chunks生成assets内容
    this.chunks.forEach(chunk => {
      const parseFileName = output.filename.replace("[name]", chunk.name);
      // assets中{"main.js": 【生成的字符串代码】}
      this.assets.set(parseFileName, getSourceCode(chunk))
    })
    // 调用Plugin emit钩子
    this.hooks.emit.call();
    // 先判断目录是否存在，存在直接fs.write 不存在则首先创建
    if (!fs.existsSync(output.path)) {
      fs.mkdirSync(output.path)
    }
    // files中保存所有的生成文件名
    this.files = Object.keys(this.assets);
    // 将assets中的内容生成打包文件 写入文件系统中;注意：开始写文件了！！！
    Array.from(this.assets.keys()).forEach(fileName => {
      const filePath = path.join(output.path, fileName);
      fs.writeFileSync(filePath, this.assets.get(fileName));
    })
    // 结束之后触发钩子
    this.hooks.done.call();
    callback(null, {
      toJson: () => {
        return {
          entries: this.entries,
          modules: this.modules,
          files: this.files,
          chunks: this.chunks,
          assets: this.assets,
        }
      }
    })
  }
}

module.exports = Compiler;


/**
 * 当我们通过new SyncHook()返回一个对象实例后
 * 我们可以通过this.hook.run.tap('name',callback)方法为这个对象上添加事件监听
 * 然后在通过this.hook.run.call()执行所有tap注册的事件
 * */
